using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace PhpDocBlockGenerator.Model.Php
{
    /// <summary>
    /// represents a php class in a file
    /// </summary>
    public class PhpClass
    {
        /// <summary>
        /// the line, where the class begins in the file (complete file, not extracted part;
        /// could be doc block begin or class keyword)
        /// </summary>
        public int BeginningLine { get; set; }

        /// <summary>
        /// the line, where the class keyword is located (extracted file part)
        /// </summary>
        public int BeginningLineClassKeywordExtractedPart { get; set; }

        /// <summary>
        /// the line, where the class keyword is located in the extracted file part
        /// </summary>
        public int BeginningLineOfExtractedPart { get; set; }

        /// <summary>
        /// class name
        /// </summary>
        public string ClassName { get; set; }

        /// <summary>
        /// the ending-line of the class in the parent-file
        /// </summary>
        public int Endline { get; set; }

        /// <summary>
        /// All lines, that shouldn't be parsed
        /// </summary>
        public List<int> ExceptedLines { get; set; }

        /// <summary>
        /// the whole class of the file in a generic list
        /// </summary>
        public List<string> FileArr { get; set; }

        /// <summary>
        /// the class-docblock generated by the program itself
        /// </summary>
        private PhpClassDocBlock _generatedDocBlock;

        /// <summary>
        /// the docblock merged from the user and the generated one
        /// </summary>
        private PhpClassDocBlock _mergedDocBlock;

        /// <summary>
        /// the class-docblock written by the user
        /// </summary>
        public PhpClassDocBlock UserDocBlock { get; set; }

        /// <summary>
        /// All Methods within a class
        /// </summary>
        private List<PhpMethod> _phpMethods = new List<PhpMethod>();

        /// <summary>
        /// All Properties within a class
        /// </summary>
        private List<PhpProperty> _phpProperties = new List<PhpProperty>();

        /// <summary>
        /// the options container
        /// </summary>
        private Options _options;

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="fileArr">list of file lines which contain the class data</param>
        /// <param name="options"></param>
        public PhpClass(List<String> fileArr, Options options)
        {
            BeginningLineOfExtractedPart = 0;
            FileArr = fileArr;
            _options = options;
        }


        /// <summary>
        /// compares and merges the generated class doc block to the existing one,
        /// calls also the compare methods of all methods, merges them
        /// and writes the merged data back to the list of file lines
        /// </summary>
        public void CompareAndMerge(PhpFileDocBlock fileDocBlock)
        {
            //merge class doc blocks
            _compareAndMergeClassDocBlocks(fileDocBlock);

            //loop over all methods and merge doc blocks
            foreach (PhpMethod m in _phpMethods)
                m.CompareAndMerge();

            //loop over all properties and merge doc blocks
            foreach (PhpProperty p in _phpProperties)
                p.CompareAndMerge();
        }

        /// <summary>
        /// executes all operations to fill the object
        /// </summary>
        public void ReadIn()
        {
            //parse methods
            _parseMethods();

            //parse properties later to eventually check the class constructor for property inits
            _parseProperties();

            //NOTE: it's ugly, but the doc block is parsed in the PhpFile-class already
            //-> no need to parse files without class doc block twice for nothing
            //parse possible doc block
            //if(null == this._UserDocBlock)
            //    this._parseClassDocBlock();

            //generate doc block
            _generateDocBlock();
        }

        /// <summary>
        /// writes the changed lines back to the list of file lines
        /// </summary>
        /// <param name="allFileLines">complete file as list of lines</param>
        /// <param name="offset">offset of earlier written lines to regard for</param>
        public void WriteToListOfFileLines(List<String> allFileLines, ref int offset)
        {
            //write doc block of class
            _writeDocBlock(allFileLines, ref offset);

            var classElements = new List<IPhpClassElements>();
            classElements.AddRange(_phpMethods);
            classElements.AddRange(_phpProperties);

            classElements = classElements.OrderBy(c => c.BeginningLine).ToList();

            //loop over all properties and write their new data to file list
            foreach (IPhpClassElements property in classElements)
            {
                //calc new position of all lines (due to earlier writing operations)
                int newPos = BeginningLine + offset;

                property.WriteToListOfFileLines(allFileLines, newPos, ref offset);
            }

            //loop over all methods and write their new data to list of file lines
            //foreach (PhpMethod method in _phpMethods)
            //{
            //    //calc new position of all lines (due to earlier writing operations in the list of lines)
            //    int newPos = BeginningLine + offset;

            //    method.WriteToListOfFileLines(allFileLines, newPos, ref offset);
            //}
        }


        /// <summary>
        /// compares and merges the generated and the read doc block
        /// </summary>
        private void _compareAndMergeClassDocBlocks(PhpFileDocBlock fileDocBlock)
        {
            //if read doc block exists
            if (null != UserDocBlock)
            {
                //compare read and generated block and merge them
                _mergedDocBlock = UserDocBlock.Compare(_generatedDocBlock);
            }
            //otherwise use generated doc block as merged block
            else
            {
                _mergedDocBlock = _generatedDocBlock;
            }

            //save file doc block data from given file doc block if there is no
            //such information at class doc block
            _mergedDocBlock.Package = fileDocBlock.Package;
            if (null == _mergedDocBlock.Copyright || 0 == _mergedDocBlock.Copyright.Description.Length)
                _mergedDocBlock.Copyright = fileDocBlock.Copyright;
        }

        /// <summary>
        /// Generates the DocBlock
        /// </summary>
        public void _generateDocBlock()
        {
            var fdb = new PhpClassDocBlock(_options);

            fdb.Author.Add(new DocBlockProperty(_options)
            {
                Description = DocBlockProperty.AuthorUnisterText,
                ObjectType = DocBlockPropertyTypes.Author
            }
            );

            fdb.Author.Add(new DocBlockProperty(_options)
            {
                Description = DocBlockProperty.AuthorStandardText,
                ObjectType = DocBlockPropertyTypes.Author
            }
            );

            fdb.Version = new DocBlockProperty(_options)
                {
                Description = DocBlockProperty.VersionStandardText,
                ObjectType = DocBlockPropertyTypes.Version
            };

            fdb.ShortDescription.Description = "<Description of " + ClassName + ">";


            _generatedDocBlock = fdb;
        }

        /// <summary>
        /// checks if a class doc block exists and parses it into
        /// a class doc block object
        /// </summary>
        private void _parseClassDocBlock()
        {
            int begin, end;
            UserDocBlock = new PhpClassDocBlock(_options) { IsUserBlock = true };

            //find out begin and end of the doc block (if there is one)
            StaticMethods.ParseDocBlockPos(FileArr, BeginningLineClassKeywordExtractedPart, out begin, out end);

            //if doc block exists
            if (begin != -1 && end != -1)
            {
                //extract it from file lines array, parse it and save position
                List<String> docBlockLines = FileArr.GetRange(begin, end - begin + 1);
                UserDocBlock.Parse(docBlockLines);
                UserDocBlock.DocBlockBegin = begin;
                UserDocBlock.DocBlockEnd = end;
            }
        }

        /// <summary>
        /// Parses all Methods
        /// </summary>
        private void _parseMethods()
        {
            // search sourcecode for "function"
            for (int i = 0; i < FileArr.Count; ++i)
            {
                // if the actual line doesnt contain one of the excepted lines
                if (!ExceptedLines.Contains(i))
                {
                    MatchCollection regFuncResult = PhpRegex.RegSearchMethodName.Matches(FileArr.ElementAt(i));

                    // if there is a match
                    if (regFuncResult.Count > 0)
                    {
                        // loop over all function matches and create new method object
                        foreach (Match funcMatch in regFuncResult)
                        {
                            // add the method to the internal method stack
                            _phpMethods.Add(new PhpMethod(FileArr, i, funcMatch.Value.Trim(), _options));
                        }
                    }
                }
            }
        }

        /// <summary>
        /// parses the class Properties
        /// </summary>
        private void _parseProperties()
        {
            // evaluate the existance of a class constructor
            PhpMethod ctor = _phpMethods.Count > 0 ? _phpMethods.SingleOrDefault(x => x.MethodName.Equals("__construct")) : null;

            //search sourcecode for properties
            for (int i = 0; i < FileArr.Count; ++i)
            {
                // if the actual line doesnt contain one of the excepted lines
                if (!ExceptedLines.Contains(i))
                {
                    MatchCollection regPropResult = PhpRegex.RegSearchPropertyName.Matches(FileArr.ElementAt(i));

                    // if there is a match
                    if (regPropResult.Count > 0)
                    {
                        bool notInMethod = _phpMethods.All(phpMethod => i <= phpMethod.BeginningLine || i >= phpMethod.EndingLine);
                        if (notInMethod)
                        {
                            foreach (Match match in regPropResult)
                            {
                                // add the properties to the internal property stack
                                _phpProperties.Add(
                                    new PhpProperty(FileArr, i, match.Value, ctor, _options));
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// writes the merged doc block to the list of file lines and removes the old one
        /// </summary>
        /// <param name="allFileLines">complete file as list of lines</param>
        /// <param name="writingOffset">offset of earlier written lines to regard for</param>
        private void _writeDocBlock(List<String> allFileLines, ref int writingOffset)
        {
            //TODO: need own exception type here
            //if no merged doc block exists then throw exception
            if (null == _mergedDocBlock)
                throw new Exception();


            //get doc block as list of lines
            List<String> docBlockAsList = _mergedDocBlock.ToList();

            //calc number of lines of old doc block (if exists)
            int numberOfLinesOldBlock = 0;
            if (null != UserDocBlock &&
                -1 != UserDocBlock.DocBlockBegin && -1 != UserDocBlock.DocBlockEnd)
            {
                numberOfLinesOldBlock =
                    UserDocBlock.DocBlockEnd - UserDocBlock.DocBlockBegin + 1;
            }

            //calc difference between old and new doc block
            int diffNumberOfLines = docBlockAsList.Count - numberOfLinesOldBlock;

            //calc new beginning position with given offset
            int newPos = BeginningLine + writingOffset;

            //remove old block and add new one
            allFileLines.RemoveRange(newPos, numberOfLinesOldBlock);
            allFileLines.InsertRange(newPos, docBlockAsList);

            //add (or remove in case of negative number) number of line difference to offset
            writingOffset += diffNumberOfLines;
        }
    }
}
